/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package cc.gehua.octopus.rpc.common.classloader;

import cc.gehua.octopus.rpc.common.resource.Resource;
import cc.gehua.octopus.rpc.common.resource.ResourceCollection;
import cc.gehua.octopus.rpc.utils.IOUtil;
import cc.gehua.octopus.rpc.utils.StringUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author herui
 */
public class OctopusClassLoader extends URLClassLoader {
    /* ------------------------------------------------------------ */

    private final Context context;
    private final ClassLoader parent;
    private final Set<String> extensions = new HashSet<>();
    private String name = String.valueOf(hashCode());
    private final List<ClassFileTransformer> transformers = new CopyOnWriteArrayList<>();
    private static final Logger LOGGER = LoggerFactory.getLogger(OctopusClassLoader.class);

    /**
     * Constructor.
     *
     * @param context
     * @throws java.io.IOException
     */
    public OctopusClassLoader(Context context)
            throws IOException {
        this(null, context);
    }

    /* ------------------------------------------------------------ */
    /**
     * Constructor.
     *
     * @param parent
     * @param context
     * @throws java.io.IOException
     */
    public OctopusClassLoader(ClassLoader parent, Context context)
            throws IOException {
        super(new URL[]{}, parent != null ? parent
                : (Thread.currentThread().getContextClassLoader() != null ? Thread.currentThread().getContextClassLoader()
                        : (OctopusClassLoader.class.getClassLoader() != null ? OctopusClassLoader.class.getClassLoader()
                                : ClassLoader.getSystemClassLoader())));
        this.parent = getParent();
        this.context = context;
        if (this.parent == null) {
            throw new IllegalArgumentException("no parent classloader!");
        }

        extensions.add(".jar");
        extensions.add(".zip");

        // TODO remove this system property
        String sextensions = System.getProperty(OctopusClassLoader.class.getName() + ".extensions");
        if (sextensions != null) {
            StringTokenizer tokenizer = new StringTokenizer(sextensions, ",;");
            while (tokenizer.hasMoreTokens()) {
                this.extensions.add(tokenizer.nextToken().trim());
            }
        }

        if (context.getExtraClasspath() != null) {
            addClassPath(context.getExtraClasspath());
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * @return the name of the classloader
     */
    public String getName() {
        return name;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param name the name of the classloader
     */
    public void setName(String name) {
        this.name = name;
    }


    /* ------------------------------------------------------------ */
    public Context getContext() {
        return context;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param resource Comma or semicolon separated path of filenames or URLs
     * pointing to directories or jar files. Directories should end with '
     * @throws java.io.IOException/'.
     */
    public void addClassPath(Resource resource)
            throws IOException {
        if (resource instanceof ResourceCollection) {
            for (Resource r : ((ResourceCollection) resource).getResources()) {
                addClassPath(r);
            }
        } else {
            addClassPath(resource.toString());
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * @param classPath Comma or semicolon separated path of filenames or URLs
     * pointing to directories or jar files. Directories should end with '/'.
     * @throws java.io.IOException
     */
    public final void addClassPath(String classPath)
            throws IOException {
        if (classPath == null) {
            return;
        }

        StringTokenizer tokenizer = new StringTokenizer(classPath, ",;");
        while (tokenizer.hasMoreTokens()) {
            Resource resource = context.newResource(tokenizer.nextToken().trim());
            LOGGER.debug("Path resource={}", resource);

            // Add the resource
            if (resource.isDirectory() && resource instanceof ResourceCollection) {
                addClassPath(resource);
            } else {
                // Resolve file path if possible
                File file = resource.getFile();
                if (file != null) {
                    URL url = resource.getURL();
                    addURL(url);
                } else if (resource.isDirectory()) {
                    addURL(resource.getURL());
                } else {
                    LOGGER.debug("Check file exists and is not nested jar: {}", resource);
                    throw new IllegalArgumentException("File not resolvable or incompatible with URLClassloader: " + resource);
                }
            }
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * @param file Checks if this file type can be added to the classpath.
     */
    private boolean isFileSupported(String file) {
        int dot = file.lastIndexOf('.');
        return dot != -1 && extensions.contains(file.substring(dot));
    }

    /* ------------------------------------------------------------ */
    /**
     * Add elements to the class path for the context from the jar and zip files
     * found in the specified resource.
     *
     * @param lib the resource that contains the jar and/or zip files.
     */
    public void addJars(Resource lib) {
        if (lib.exists() && lib.isDirectory()) {
            String[] files = lib.list();
            for (int f = 0; files != null && f < files.length; f++) {
                try {
                    Resource fn = lib.addPath(files[f]);
                    String fnlc = fn.getName().toLowerCase(Locale.ENGLISH);
                    // don't check if this is a directory, see Bug 353165
                    if (isFileSupported(fnlc)) {
                        String jar = fn.toString();
                        jar = StringUtil.replace(jar, ",", "%2C");
                        jar = StringUtil.replace(jar, ";", "%3B");
                        addClassPath(jar);
                    }
                } catch (Exception ex) {
                    LOGGER.warn("AddJars failed.", ex);
                }
            }
        }
    }

    /* ------------------------------------------------------------ */
    @Override
    public PermissionCollection getPermissions(CodeSource cs) {
        PermissionCollection permissions = context.getPermissions();
        PermissionCollection pc = (permissions == null) ? super.getPermissions(cs) : permissions;
        return pc;
    }

    /* ------------------------------------------------------------ */
    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        boolean system_class = context.isSystemClass(name);
        boolean server_class = context.isServerClass(name);

        List<URL> from_parent = toList(server_class ? null : parent.getResources(name));
        List<URL> from_webapp = toList((system_class && !from_parent.isEmpty()) ? null : this.findResources(name));

        if (context.isParentLoaderPriority()) {
            from_parent.addAll(from_webapp);
            return Collections.enumeration(from_parent);
        }
        from_webapp.addAll(from_parent);
        return Collections.enumeration(from_webapp);
    }

    /* ------------------------------------------------------------ */
    private List<URL> toList(Enumeration<URL> e) {
        if (e == null) {
            return new ArrayList<>();
        }
        return Collections.list(e);
    }

    /* ------------------------------------------------------------ */
    /**
     * Get a resource from the classloader
     *
     * NOTE: this method provides a convenience of hacking off a leading /
     * should one be present. This is non-standard and it is recommended to not
     * rely on this behavior
     *
     * @return
     */
    @Override
    public URL getResource(String name) {
        URL url = null;
        boolean tried_parent = false;

        //If the resource is a class name with .class suffix, strip it off before comparison
        //as the server and system patterns are specified without a .class suffix
        String tmp = name;
        if (tmp != null && tmp.endsWith(".class")) {
            tmp = tmp.substring(0, tmp.length() - 6);
        }

        boolean system_class = context.isSystemClass(tmp);
        boolean server_class = context.isServerClass(tmp);

        if (system_class && server_class) {
            return null;
        }

        if (parent != null && (context.isParentLoaderPriority() || system_class) && !server_class) {
            tried_parent = true;

            if (parent != null) {
                url = parent.getResource(name);
            }
        }

        if (url == null) {
            url = this.findResource(name);

            if (url == null && name.startsWith("/")) {
                LOGGER.debug("HACK leading / off {}", name);
                url = this.findResource(name.substring(1));
            }
        }

        if (url == null && !tried_parent && !server_class) {
            if (parent != null) {
                url = parent.getResource(name);
            }
        }

        if (url != null) {
            LOGGER.debug("getResource({})=", url);
        }

        return url;
    }

    /* ------------------------------------------------------------ */
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    /* ------------------------------------------------------------ */
    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(name);
        ClassNotFoundException ex = null;
        boolean tried_parent = false;

        boolean system_class = context.isSystemClass(name);
        boolean server_class = context.isServerClass(name);

        if (system_class && server_class) {
            return null;
        }

        if (c == null && parent != null && (context.isParentLoaderPriority() || system_class) && !server_class) {
            tried_parent = true;
            try {
                c = parent.loadClass(name);
                LOGGER.debug("loaded {}", c);
            } catch (ClassNotFoundException e) {
                ex = e;
            }
        }

        if (c == null) {
            try {
                c = this.findClass(name);
            } catch (ClassNotFoundException e) {
                ex = e;
            }
        }

        if (c == null && parent != null && !tried_parent && !server_class) {
            c = parent.loadClass(name);
        }

        if (c == null && ex != null) {
            throw ex;
        }

        if (resolve) {
            resolveClass(c);
        }

        LOGGER.debug("loaded {} from {}", c, c == null ? null : c.getClassLoader());

        return c;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param transformer
     * @see addTransformer
     * @deprecated
     */
    public void addClassFileTransformer(ClassFileTransformer transformer) {
        transformers.add(transformer);
    }

    /* ------------------------------------------------------------ */
    /**
     * @param transformer
     * @return
     * @see removeTransformer
     * @deprecated
     */
    public boolean removeClassFileTransformer(ClassFileTransformer transformer) {
        return transformers.remove(transformer);
    }

    /* ------------------------------------------------------------ */
    /**
     * @param transformer
     * @see addClassFileTransformer
     */
    public void addTransformer(ClassFileTransformer transformer) {
        transformers.add(transformer);
    }

    /* ------------------------------------------------------------ */
    /**
     * @param transformer
     * @return
     * @see removeClassFileTransformer
     */
    public boolean removeTransformer(ClassFileTransformer transformer) {
        return transformers.remove(transformer);
    }

    /* ------------------------------------------------------------ */
    @Override
    protected Class<?> findClass(final String name) throws ClassNotFoundException {
        Class<?> clazz = null;

        if (transformers.isEmpty()) {
            clazz = super.findClass(name);
        } else {
            String path = name.replace('.', '/').concat(".class");
            URL url = getResource(path);
            if (url == null) {
                throw new ClassNotFoundException(name);
            }

            InputStream content = null;
            try {
                content = url.openStream();
                byte[] bytes = IOUtil.readBytes(content);

                for (ClassFileTransformer transformer : transformers) {
                    bytes = transformer.transform(this, name, null, null, bytes);
                }

                clazz = defineClass(name, bytes, 0, bytes.length);
            } catch (IOException | IllegalClassFormatException e) {
                throw new ClassNotFoundException(name, e);
            } finally {
                if (content != null) {
                    try {
                        content.close();
                    } catch (IOException e) {
                        throw new ClassNotFoundException(name, e);
                    }
                }
            }
        }

        return clazz;
    }

    /* ------------------------------------------------------------ */
    @Override
    public String toString() {
        return "OctopusClassLoader=" + name + "@" + Long.toHexString(hashCode());
    }
}
